package handler

import (
	"compress/gzip"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"chainmaker.org/chainmaker/smarteditor/conf"
	"chainmaker.org/chainmaker/smarteditor/container"
	"chainmaker.org/chainmaker/smarteditor/monitor"
	"chainmaker.org/chainmaker/smarteditor/session"
	"chainmaker.org/chainmaker/smarteditor/util"
	"github.com/prometheus/client_golang/prometheus"
)

type HttpHandler func(response http.ResponseWriter, request *http.Request)

func TimeCostAndAuthInterceptor(handler HttpHandler) HttpHandler {
	return panicLog(logTimeWrapper(authWrapper(handler)))
}

func TimeCostAndAuthAndResourcesLimitInterceptor(handler HttpHandler) HttpHandler {
	return panicLog(logTimeWrapper(authWrapper(systemResourceLimitWrapper(handler))))
}

//CompressAndLogTimeCost log time and compress resources
func CompressAndLogTimeCostInterceptor(handler HttpHandler) HttpHandler {
	return panicLog(logTimeWrapper(compressWrapper(handler)))
}

func TimeCostInterceptor(handler HttpHandler) HttpHandler {
	return panicLog(logTimeWrapper(handler))
}

//panicLog panic log if handler run fail
func panicLog(handler HttpHandler) HttpHandler {
	return func(response http.ResponseWriter, request *http.Request) {
		defer util.PanicLog()
		handler(response, request)
	}
}

//logTimeWrapper record request time cost
func logTimeWrapper(handler HttpHandler) HttpHandler {
	return func(response http.ResponseWriter, request *http.Request) {
		start := time.Now()
		if !conf.Wide.EnablePrometheus {
			handler(response, request)
			container.SC.Logger.Debugf("[TimeLogDecorator] time cost: (method:%s, uri:%s, cost:%s)",
				request.Method, request.RequestURI, time.Since(start))
		} else {
			newResp := &statusCodeWriter{ResponseWriter: response}
			handler(newResp, request)
			usedT := time.Since(start)
			container.SC.Logger.Debugf("[TimeLogDecorator] time cost: (method:%s, uri:%s, cost:%s)", request.Method, request.RequestURI, usedT)
			statusCode := strconv.Itoa(newResp.StatusCode) // todo
			if newResp.StatusCode == 0 {
				statusCode = "200"
			}
			rui := request.RequestURI
			ind := strings.Index(request.RequestURI, "?")
			if ind > 0 {
				rui = rui[:ind]
			}
			apiHisto.WithLabelValues(rui, statusCode).Observe(usedT.Seconds())
			apiTotal.WithLabelValues(rui, statusCode).Inc()
		}
	}
}

func authWrapper(handler HttpHandler) HttpHandler {
	return func(response http.ResponseWriter, request *http.Request) {
		//skip auth check if use debug module
		if conf.Wide.SkipAuth {
			handler(response, request)
			return
		}

		_, err := session.GetUidFromSession(request)
		if err != nil {
			http.Error(response, "forbidden", http.StatusForbidden)
			container.SC.Logger.Debugf("[authWrapper] session is illegal. err: %v", err)
			return
		}

		//check session
		httpSession, _ := session.HTTPSession.Get(request, session.CookieName)
		sessionId := httpSession.Values["id"]

		if sessionId == nil || !container.SC.SessionManager.Exists(sessionId.(string)) {
			container.SC.Logger.Debugf("[authWrapper] session not found. err: %v", err)
			//http.Redirect(response, request, "/login", http.StatusFound)
			http.Error(response, "Forbidden", http.StatusForbidden)
			return
		}

		handler(response, request)
	}
}

//
func systemResourceLimitWrapper(handler HttpHandler) HttpHandler {
	return func(response http.ResponseWriter, request *http.Request) {
		if conf.Wide.OpenLimit {
			getResourceSuccess := container.SC.SystemResourcePool.Get()
			if !getResourceSuccess {
				container.SC.Logger.Warnf("[systemResourceLimitWrapper], get resource fail")
				http.Error(response, "resource limit", http.StatusServiceUnavailable)
				return
			}
			defer container.SC.SystemResourcePool.Put()
		}

		handler(response, request)
	}
}

//compressWrapper compress resource
func compressWrapper(handler HttpHandler) HttpHandler {
	return func(response http.ResponseWriter, request *http.Request) {
		if !strings.Contains(request.Header.Get("Accept-Encoding"), "gzip") {
			handler(response, request)
			return
		}

		response.Header().Set("Content-Encoding", "gzip")
		gzipWriter := gzip.NewWriter(response)
		defer util.CloseAndLogError(gzipWriter)

		handler(gzipResponseWriter{Writer: gzipWriter, ResponseWriter: response}, request)
	}
}

//
func corsWrapper(handler HttpHandler) HttpHandler {
	return func(response http.ResponseWriter, request *http.Request) {
		method := request.Method

		origin := request.Header.Get("Origin")
		response.Header().Set("Access-Control-Allow-Origin", origin)
		response.Header().Set("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id,WithCredentials")
		response.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT")
		response.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At")
		response.Header().Set("Access-Control-Allow-Credentials", "true")
		if method == "OPTIONS" {
			response.WriteHeader(http.StatusNoContent)
			return
		}
		handler(response, request)
	}
}

// gzipResponseWriter represents a gzip response writer.
type gzipResponseWriter struct {
	io.Writer
	http.ResponseWriter
}

// Write writes response with appropriate 'Content-Type'.
func (w gzipResponseWriter) Write(b []byte) (int, error) {
	if w.Header().Get("Content-Type") == "" {
		// If no content type, apply sniffing algorithm to un-gzipped body.
		w.Header().Set("Content-Type", http.DetectContentType(b))
	}
	return w.Writer.Write(b)
}

// 下面增加监控模块信息
var (
	//apiGauage *prometheus.GaugeVec
	apiHisto *prometheus.HistogramVec
	apiTotal *prometheus.CounterVec
)

func InitMonitorSubName(subName string) {
	//apiGauage = monitor.NewGaugeVec(subName, "http_process_duration", "request process time", "path")
	apiHisto = monitor.NewHistogramVec(subName, "http_histogram", "process consume time histogram",
		[]float64{0.01, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 20.0},
		"path", "statusCode")
	apiTotal = monitor.NewCounterVec(subName, "http_total", "request total count", "path", "statusCode")
}

type statusCodeWriter struct {
	StatusCode int
	http.ResponseWriter
}

func (w *statusCodeWriter) WriteHeader(statusCode int) {
	w.StatusCode = statusCode
	w.ResponseWriter.WriteHeader(statusCode)
}
